package com.stottlerhenke.simbionic.engine.parser;

import java.util.*;
import com.stottlerhenke.simbionic.engine.core.*;
import com.stottlerhenke.simbionic.api.*;
import com.stottlerhenke.simbionic.engine.*;
import com.stottlerhenke.simbionic.common.SIM_Constants;
import com.stottlerhenke.simbionic.common.SB_Logger;
import com.stottlerhenke.simbionic.common.classes.SB_ClassDescription;
import com.stottlerhenke.simbionic.common.classes.SB_ClassUtil;
import com.stottlerhenke.simbionic.common.classes.SB_NoSuchMethodException;

import java.lang.reflect.*;

/**
 *
 * A class used to represent class method calls (both static and non-static).
 * 
 * It is derived from SB_ExpFunction to make parsing simpler.
 */
public class SB_ExpMethod extends SB_ExpFunction
{
	/**
	 * @param fun
	 */
	public SB_ExpMethod(String methodName)
	{
		super(null);
		_methodName = methodName;
	}
	
	public String PrintTree() throws SB_Exception{
    return new String("method(" + _methodName + ")");
  }
	
	public EExpressionType GetType()
	{
    return EExpressionType.kClassMethod;
  }
	
  /**
   * Replaces all instances of SB_Method in the expression tree that
   * match the name of newMethod.
   * @param newMethod the method with which to replace existing methods
   */
  public void replaceFunction(SB_Function newMethod)
  {
      for (int i=0; i<_args.size(); ++i)
      {
          ((SB_Expression)_args.get(i)).replaceFunction(newMethod);
      }
  }
	
  /**
   * Class methods aren't useful without the class that they belong to. Because of this,
   * this function simply returns this node so it can be evaluated later with all of the 
   * required information.
   * 
   * Methods may have side effects. No attempt is made to save deep arguments (such as arrays).
   * 
   * @return this expression node as a data object
   */
  public SB_Variable Evaluate(SB_Entity entity, SB_ExecutionFrame contextFrame) throws SB_Exception
  {
  	return new SB_VarData(this);
  }
  
  public SB_Variable Evaluate(SB_Entity entity, SB_ExecutionFrame contextFrame, SB_VarClass sbClass) throws SB_Exception
  {
    ArrayList args = new ArrayList();
    SB_Expression expIt;
    for(int i = 0; i < _args.size(); i++){
      expIt =(SB_Expression) _args.get(i);
      args.add(expIt.Evaluate(entity, contextFrame));
    } 
       
    ArrayList params = new ArrayList();
    params = SB_SimInterface.ConvertObjects(args);
    long id = SB_SimInterface.TranslateId(entity.GetId());

    //TODO how does this work with debugging?
    
    if (SIM_Constants.DEBUG_INFO_ON)
      contextFrame.GetLogger().log("[" + entity.toString() + ",STK " + contextFrame.GetStackDepth()
      + "] PRED:\tinvoking " + sbClass.getClassClass() + "." + _methodName + "(...)",SB_Logger.PREDICATE);

    SB_Variable retVal = null;
    
    try 
		{
    	retVal = callClassMethod(sbClass, params, contextFrame.GetBook());
		}
    catch(SB_Exception ex)
		{  
	    if (SIM_Constants.DEBUG_INFO_ON)
	    {	 
	      String exceptionMethodString = "Exception thrown during call to: " + createMethodString(sbClass.getClassClass(), _methodName, params);
	        
	      //add extra information
	      if( sbClass.getClassObject() == null )
	        exceptionMethodString = exceptionMethodString + "This may be because the SimBionic variable making the method call contains a null value and the method being called is not static. ";

	      //Log the error
	    	contextFrame.GetLogger().log("[" + entity.toString() + ",STK " + contextFrame.GetStackDepth()
	          + "] PRED:\t" + exceptionMethodString,SB_Logger.ERROR);
	    	contextFrame.GetLogger().logException(ex);
	    }  	
    	
    	SB_EntityData state = entity.GetState();
  		state.GetExecStack().HandleException( ex, contextFrame);
  		return null;
		}
    
    if (SIM_Constants.DEBUG_INFO_ON)
      contextFrame.GetLogger().log("[" + entity.toString() + ",STK " + contextFrame.GetStackDepth()
      + "] PRED:\tcompleted",SB_Logger.PREDICATE);

    return retVal;
  }

  /**
   * Create a string describing a class method call
   * 
   * @param sbClass
   * @param params an array of Objects
   * @return
   */
  protected static String createMethodString(Object sbClass, String methodName, ArrayList params)
  {
  	StringBuffer buf = new StringBuffer();
  	buf.append(sbClass.toString() + "." + methodName + "(");
  	
  	int count = params.size();
  	for( int x = 0; x < count; x++ )
  	{
  		if( x > 0 )
  			buf.append(",");
  		
  		if( params.get(x) != null)
  		  buf.append(params.get(x).toString());
  		else
  		  buf.append("null");
  	}
  	buf.append(")");
  
  	return buf.toString();
  }
  
  /**
   * Call the named method on the given class
   * @param sbClass the class
   * @param params an array of Objects ready for transfer
   * @return
   */
  protected SB_Variable callClassMethod(SB_VarClass sbClass, ArrayList params, SB_SingletonBook book)
  	throws SB_Exception
  {
  	return EvaluateClassMemberMethod( sbClass.getClassClass(), sbClass.getClassObject(), _methodName, params, book);
  }
  
  static private SB_Variable EvaluateSuperClassOrInterfaceMemberMethod(Class cls, Object obj, String methodName, ArrayList params, SB_SingletonBook book) throws SB_Exception
  {
	  ArrayList supers = new ArrayList();
	  if (cls.getSuperclass() != null)
		  supers.add(cls.getSuperclass());
	  
	  Class[] interfaces = cls.getInterfaces();
	  for (int i = 0; i < interfaces.length; i ++)
		  supers.add(interfaces[i]);
	  
	  for (int i = 0; i < supers.size(); i ++)
	  {
		  try{
			  return EvaluateClassMemberMethod((Class)supers.get(i), obj, methodName, params, book);
		  }
		  catch (SB_NoSuchMethodException ex2){
		  }
	  }
	  
      throw new SB_NoSuchMethodException("Could not find a SimBionic method description matching the following name and parameters: " +
      		SB_ClassUtil.getMethodString(cls.getName(), methodName, params));
  }
  
  /**
   * 
   * Evaluate a class member method
   * 
   * @param cls the class
   * @param obj the object of type class
   * @param methodName the method name
   * @param params a list of Objects. These must match the method specification exactly.
   * @return null if the class member isn't found or the return value is null, otherwise the return value of the method which may also be null
   * @throws Exception
   */
  static public SB_Variable EvaluateClassMemberMethod(Class cls, Object obj, String methodName, ArrayList params, SB_SingletonBook book)
		throws SB_Exception
	{
      SB_Variable result = null;
      
      //Get just the end of the class name
      String className = cls.getName();
      int dotIndex = className.lastIndexOf(".");
      if( dotIndex >= 0 )
          className = className.substring(dotIndex + 1);
      
      if(methodName.compareTo(className) != 0 ) //Method
      {
          SB_ClassDescription description = book.getUserClassMap().getClassDescription(cls.getName());
          if(description == null)
          {
        	  return EvaluateSuperClassOrInterfaceMemberMethod(cls, obj, methodName, params, book);
//        	  if (cls.getSuperclass() != null)
//        		  return EvaluateClassMemberMethod(cls.getSuperclass(), obj, methodName, params, book);
//        	  else
//        		  throw new SB_Exception("No class description found for: " + cls.getName());
          }
          else{
        	  try{
                  Object o = description.invokeMatchingMethod(book, obj, cls, methodName, params);
                  SB_Param p = SB_SimInterface.ConvertObjectToSB_Param(o);
                  return SB_SimInterface.ConvertParamAuto( p, book);
        	  }
        	  catch (SB_NoSuchMethodException ex)
        	  {
            	  return EvaluateSuperClassOrInterfaceMemberMethod(cls, obj, methodName, params, book);
        	  }
//        	  catch (SB_Exception ex)
//        	  {
//        		  if (ex instanceof SB_NoSuchMethodException && cls.getSuperclass() != null)
//        			  return EvaluateClassMemberMethod(cls.getSuperclass(), obj, methodName, params, book);
//        		  else
//        			  throw ex;
//        	  }
          }
      }
      else //Constructor
      {
          if (SIM_Constants.DEBUG_INFO_ON)
  			book.getLogger().log("Attempting to call the following constructor: " + SB_ClassUtil.getMethodString(className, methodName, params));

          SB_ClassDescription desc = book.getUserClassMap().getClassDescription(cls.getName());
          if(desc == null)
              throw new SB_Exception("No SimBionic class description found for the following class: " + cls.getName());

          Constructor con = desc.getMatchingConstructor(className, cls, params);
                    
          try
          {    
              Object o = con.newInstance(params.toArray());
              SB_Param p = SB_SimInterface.ConvertObjectToSB_Param(o);
              return SB_SimInterface.ConvertParamAuto( p, book);
          }
          catch(InstantiationException ex)
          {
              throw new SB_Exception("Java cannot call the given constructor because the class is an interface or is an abstract class: " + con.toString() , ex);
          }
          catch(IllegalAccessException ex)
          {
              throw new SB_Exception("Access to the following methods was denied by Java security: " + con.toString() , ex);
              
          }
          catch(IllegalArgumentException ex)
          {
              throw new SB_Exception("The Java constructor " + con.toString() + " has been passed an illegal or inappropriate argument by this call: " + 
                      SB_ClassUtil.getMethodString(className, className, params), ex);
         }
          catch(InvocationTargetException ex)
          {
              if( ex.getTargetException() != null)
                  throw new SB_Exception("An exception was thrown by the constructor: " + con.toString(), ex.getTargetException());
              else
                  throw new SB_Exception("An unspecified exception was thrown by the constructor: " + con.toString(), ex);
          }
          
      }
	}
  
	protected String _methodName;
}
